1use std::{
2 collections::{HashMap, HashSet},
3 ffi::CStr,
4 fmt::Debug,
5 sync::{Arc, Mutex, RwLock},
6};
7
8use crate::{
9 data::object::ObsObjectTrait,
10 encoders::{audio::ObsAudioEncoder, video::ObsVideoEncoder},
11 enums::ObsOutputStopSignal,
12 macros::trait_with_optional_send_sync,
13 run_with_obs,
14 runtime::ObsRuntime,
15 services::ObsServiceRef,
16 utils::{AudioEncoderInfo, ObsError, OutputInfo, VideoEncoderInfo},
17};
18
19use super::ObsOutputSignals;
20
21#[derive(Clone, Debug, Default)]
22pub struct ObsOutputComposition {
29 video_encoder: Option<Arc<ObsVideoEncoder>>,
30 audio_encoders: HashMap<usize, Arc<ObsAudioEncoder>>,
31 service: Option<Arc<ObsServiceRef>>,
32}
33
34impl ObsOutputComposition {
35 pub fn new() -> Self {
36 Self::default()
37 }
38
39 pub fn with_video_encoder(mut self, encoder: Arc<ObsVideoEncoder>) -> Self {
40 self.video_encoder = Some(encoder);
41 self
42 }
43
44 pub fn with_audio_encoder(mut self, mixer_idx: usize, encoder: Arc<ObsAudioEncoder>) -> Self {
45 self.audio_encoders.insert(mixer_idx, encoder);
46 self
47 }
48
49 pub fn with_service(mut self, service: Arc<ObsServiceRef>) -> Self {
50 self.service = Some(service);
51 self
52 }
53
54 pub fn without_video_encoder(mut self) -> Self {
55 self.video_encoder = None;
56 self
57 }
58
59 pub fn without_audio_encoder(mut self, mixer_idx: usize) -> Self {
60 self.audio_encoders.remove(&mixer_idx);
61 self
62 }
63
64 pub fn without_service(mut self) -> Self {
65 self.service = None;
66 self
67 }
68
69 pub fn video_encoder(&self) -> Option<&Arc<ObsVideoEncoder>> {
70 self.video_encoder.as_ref()
71 }
72
73 pub fn audio_encoders(&self) -> &HashMap<usize, Arc<ObsAudioEncoder>> {
74 &self.audio_encoders
75 }
76
77 pub fn service(&self) -> Option<&Arc<ObsServiceRef>> {
78 self.service.as_ref()
79 }
80}
81
82trait_with_optional_send_sync! {
83 pub(crate) trait ObsOutputTraitSealed: Debug {
84 fn new(output: OutputInfo, runtime: ObsRuntime) -> Result<Self, ObsError>
86 where
87 Self: Sized;
88
89 fn video_encoder_slot(&self) -> &Arc<RwLock<Option<Arc<ObsVideoEncoder>>>>;
90 fn audio_encoder_slots(&self) -> &Arc<RwLock<HashMap<usize, Arc<ObsAudioEncoder>>>>;
91 fn service_slot(&self) -> &Arc<RwLock<Option<Arc<ObsServiceRef>>>>;
92 fn configuration_lock(&self) -> &Arc<Mutex<()>>;
93 }
94}
95
96#[allow(private_bounds)]
97pub trait ObsOutputTrait:
98 ObsOutputTraitSealed + ObsObjectTrait<Native = *mut libobs::obs_output_t>
99{
100 fn signals(&self) -> &Arc<ObsOutputSignals>;
101
102 #[doc(hidden)]
103 fn native_output_flags(&self) -> Result<u32, ObsError> {
104 let output_ptr = self.__native_handle();
105 let runtime = self.runtime().clone();
106 run_with_obs!(runtime, (output_ptr), move || {
107 unsafe { libobs::obs_output_get_flags(output_ptr.get_ptr()) }
109 })
110 }
111
112 fn attached_video_encoder(&self) -> Result<Option<Arc<ObsVideoEncoder>>, ObsError> {
114 let curr = self
115 .video_encoder_slot()
116 .read()
117 .map_err(|e| ObsError::LockError(e.to_string()))?;
118
119 Ok(curr.clone())
120 }
121
122 fn attached_audio_encoder(
124 &self,
125 mixer_idx: usize,
126 ) -> Result<Option<Arc<ObsAudioEncoder>>, ObsError> {
127 self.audio_encoder_slots()
128 .read()
129 .map_err(|e| ObsError::LockError(e.to_string()))
130 .map(|encoders| encoders.get(&mixer_idx).cloned())
131 }
132
133 fn attached_audio_encoders(&self) -> Result<HashMap<usize, Arc<ObsAudioEncoder>>, ObsError> {
135 self.audio_encoder_slots()
136 .read()
137 .map_err(|e| ObsError::LockError(e.to_string()))
138 .map(|encoders| encoders.clone())
139 }
140
141 fn attached_service(&self) -> Result<Option<Arc<ObsServiceRef>>, ObsError> {
143 self.service_slot()
144 .read()
145 .map_err(|e| ObsError::LockError(e.to_string()))
146 .map(|service| service.clone())
147 }
148
149 #[deprecated = "Use attached_video_encoder"]
150 fn get_current_video_encoder(&self) -> Result<Option<Arc<ObsVideoEncoder>>, ObsError> {
151 self.attached_video_encoder()
152 }
153
154 #[deprecated = "Use attached_audio_encoder"]
155 fn get_current_audio_encoder(
156 &self,
157 mixer_idx: usize,
158 ) -> Result<Option<Arc<ObsAudioEncoder>>, ObsError> {
159 self.attached_audio_encoder(mixer_idx)
160 }
161
162 #[deprecated = "Use attached_audio_encoders"]
163 fn get_current_audio_encoders(&self) -> Result<HashMap<usize, Arc<ObsAudioEncoder>>, ObsError> {
164 self.attached_audio_encoders()
165 }
166
167 #[deprecated = "Use attached_service"]
168 fn get_current_service(&self) -> Result<Option<Arc<ObsServiceRef>>, ObsError> {
169 self.attached_service()
170 }
171
172 fn current_composition(&self) -> Result<ObsOutputComposition, ObsError> {
174 let _configuration = self
175 .configuration_lock()
176 .lock()
177 .map_err(|e| ObsError::LockError(e.to_string()))?;
178 Ok(ObsOutputComposition {
179 video_encoder: self.attached_video_encoder()?,
180 audio_encoders: self.attached_audio_encoders()?,
181 service: self.attached_service()?,
182 })
183 }
184
185 fn set_service(&self, service: Arc<ObsServiceRef>) -> Result<(), ObsError> {
187 let _configuration = self
188 .configuration_lock()
189 .lock()
190 .map_err(|e| ObsError::LockError(e.to_string()))?;
191 if self.is_active()? {
192 return Err(ObsError::OutputAlreadyActive);
193 }
194 let flags = self.native_output_flags()?;
195 ensure_output_supports_service(self, flags)?;
196 self.runtime().ensure_same_runtime(service.runtime())?;
197
198 let mut slot = self
199 .service_slot()
200 .write()
201 .map_err(|e| ObsError::LockError(e.to_string()))?;
202 let output_ptr = self.__native_handle();
203 let service_ptr = service.__native_handle();
204 let runtime = self.runtime().clone();
205 run_with_obs!(runtime, (output_ptr, service_ptr), move || {
206 unsafe { libobs::obs_output_set_service(output_ptr.get_ptr(), service_ptr.get_ptr()) };
208 })?;
209
210 slot.replace(service);
211 Ok(())
212 }
213
214 fn apply_composition(&self, composition: ObsOutputComposition) -> Result<(), ObsError> {
220 let _configuration = self
221 .configuration_lock()
222 .lock()
223 .map_err(|e| ObsError::LockError(e.to_string()))?;
224 if self.is_active()? {
225 return Err(ObsError::OutputAlreadyActive);
226 }
227 let flags = self.native_output_flags()?;
228 if composition.video_encoder.is_some() {
229 ensure_output_supports_video_encoder(self, flags)?;
230 }
231 if !composition.audio_encoders.is_empty() {
232 ensure_output_supports_audio_encoder(self, flags)?;
233 }
234 if composition.service.is_some() {
235 ensure_output_supports_service(self, flags)?;
236 }
237
238 for mixer_idx in composition.audio_encoders.keys() {
239 validate_audio_mixer(*mixer_idx)?;
240 }
241 if let Some(encoder) = composition.video_encoder.as_ref() {
242 self.runtime().ensure_same_runtime(encoder.runtime())?;
243 }
244 for encoder in composition.audio_encoders.values() {
245 self.runtime().ensure_same_runtime(encoder.runtime())?;
246 }
247 if let Some(service) = composition.service.as_ref() {
248 self.runtime().ensure_same_runtime(service.runtime())?;
249 }
250
251 let mut video_slot = self
254 .video_encoder_slot()
255 .write()
256 .map_err(|e| ObsError::LockError(e.to_string()))?;
257 let mut audio_slots = self
258 .audio_encoder_slots()
259 .write()
260 .map_err(|e| ObsError::LockError(e.to_string()))?;
261 let mut service_slot = self
262 .service_slot()
263 .write()
264 .map_err(|e| ObsError::LockError(e.to_string()))?;
265
266 let output_ptr = self.__native_handle();
267 let video_ptr = composition
268 .video_encoder
269 .as_ref()
270 .map(|encoder| encoder.__native_handle());
271 let service_ptr = composition
272 .service
273 .as_ref()
274 .map(|service| service.__native_handle());
275 let audio_ptrs = composition
276 .audio_encoders
277 .iter()
278 .map(|(mixer_idx, encoder)| (*mixer_idx, encoder.__native_handle()))
279 .collect::<HashMap<_, _>>();
280 let mixer_indices = audio_slots
281 .keys()
282 .copied()
283 .chain(audio_ptrs.keys().copied())
284 .collect::<HashSet<_>>();
285 let runtime = self.runtime().clone();
286
287 run_with_obs!(
288 runtime,
289 (
290 output_ptr,
291 video_ptr,
292 service_ptr,
293 audio_ptrs,
294 mixer_indices,
295 flags
296 ),
297 move || unsafe {
298 if output_supports_video_encoder(flags) {
303 libobs::obs_output_set_video_encoder(
304 output_ptr.get_ptr(),
305 video_ptr
306 .as_ref()
307 .map_or(std::ptr::null_mut(), |encoder| encoder.get_ptr()),
308 );
309 }
310 if output_supports_audio_encoder(flags) {
311 for mixer_idx in mixer_indices {
312 libobs::obs_output_set_audio_encoder(
313 output_ptr.get_ptr(),
314 audio_ptrs
315 .get(&mixer_idx)
316 .map_or(std::ptr::null_mut(), |encoder| encoder.get_ptr()),
317 mixer_idx,
318 );
319 }
320 }
321 if output_supports_service(flags) {
322 libobs::obs_output_set_service(
323 output_ptr.get_ptr(),
324 service_ptr
325 .as_ref()
326 .map_or(std::ptr::null_mut(), |service| service.get_ptr()),
327 );
328 }
329 }
330 )?;
331
332 *video_slot = composition.video_encoder;
333 *audio_slots = composition.audio_encoders;
334 *service_slot = composition.service;
335 Ok(())
336 }
337
338 fn clear_video_encoder(&self) -> Result<(), ObsError> {
340 let _configuration = self
341 .configuration_lock()
342 .lock()
343 .map_err(|e| ObsError::LockError(e.to_string()))?;
344 if self.is_active()? {
345 return Err(ObsError::OutputAlreadyActive);
346 }
347 let flags = self.native_output_flags()?;
348 let output_ptr = self.__native_handle();
349 let runtime = self.runtime().clone();
350 let mut slot = self
351 .video_encoder_slot()
352 .write()
353 .map_err(|e| ObsError::LockError(e.to_string()))?;
354 if output_supports_video_encoder(flags) {
355 run_with_obs!(runtime, (output_ptr), move || unsafe {
356 libobs::obs_output_set_video_encoder(output_ptr.get_ptr(), std::ptr::null_mut());
358 })?;
359 }
360 *slot = None;
361 Ok(())
362 }
363
364 fn clear_audio_encoder(&self, mixer_idx: usize) -> Result<(), ObsError> {
366 let _configuration = self
367 .configuration_lock()
368 .lock()
369 .map_err(|e| ObsError::LockError(e.to_string()))?;
370 validate_audio_mixer(mixer_idx)?;
371 if self.is_active()? {
372 return Err(ObsError::OutputAlreadyActive);
373 }
374 let flags = self.native_output_flags()?;
375 let output_ptr = self.__native_handle();
376 let runtime = self.runtime().clone();
377 let mut slots = self
378 .audio_encoder_slots()
379 .write()
380 .map_err(|e| ObsError::LockError(e.to_string()))?;
381 if output_supports_audio_encoder(flags) {
382 run_with_obs!(runtime, (output_ptr), move || unsafe {
383 libobs::obs_output_set_audio_encoder(
385 output_ptr.get_ptr(),
386 std::ptr::null_mut(),
387 mixer_idx,
388 );
389 })?;
390 }
391 slots.remove(&mixer_idx);
392 Ok(())
393 }
394
395 fn clear_service(&self) -> Result<(), ObsError> {
397 let _configuration = self
398 .configuration_lock()
399 .lock()
400 .map_err(|e| ObsError::LockError(e.to_string()))?;
401 if self.is_active()? {
402 return Err(ObsError::OutputAlreadyActive);
403 }
404 let flags = self.native_output_flags()?;
405 let output_ptr = self.__native_handle();
406 let runtime = self.runtime().clone();
407 let mut slot = self
408 .service_slot()
409 .write()
410 .map_err(|e| ObsError::LockError(e.to_string()))?;
411 if output_supports_service(flags) {
412 run_with_obs!(runtime, (output_ptr), move || unsafe {
413 libobs::obs_output_set_service(output_ptr.get_ptr(), std::ptr::null_mut());
415 })?;
416 }
417 *slot = None;
418 Ok(())
419 }
420
421 fn create_and_set_video_encoder(
425 &self,
426 info: VideoEncoderInfo,
427 ) -> Result<Arc<ObsVideoEncoder>, ObsError> {
428 if self.is_active()? {
429 return Err(ObsError::OutputAlreadyActive);
430 }
431 ensure_output_supports_video_encoder(self, self.native_output_flags()?)?;
432
433 let video_enc = ObsVideoEncoder::new_from_info(info, self.runtime().clone())?;
434
435 self.set_video_encoder(video_enc.clone())?;
436 Ok(video_enc)
437 }
438
439 fn set_video_encoder(&self, encoder: Arc<ObsVideoEncoder>) -> Result<(), ObsError> {
443 let _configuration = self
444 .configuration_lock()
445 .lock()
446 .map_err(|e| ObsError::LockError(e.to_string()))?;
447 if self.is_active()? {
448 return Err(ObsError::OutputAlreadyActive);
449 }
450 ensure_output_supports_video_encoder(self, self.native_output_flags()?)?;
451 self.runtime().ensure_same_runtime(encoder.runtime())?;
452
453 let mut slot = self
454 .video_encoder_slot()
455 .write()
456 .map_err(|e| ObsError::LockError(e.to_string()))?;
457 let output_ptr = self.__native_handle();
458 let encoder_ptr = encoder.__native_handle();
459 let runtime = self.runtime().clone();
460
461 run_with_obs!(runtime, (output_ptr, encoder_ptr), move || {
462 unsafe {
463 libobs::obs_output_set_video_encoder(output_ptr.get_ptr(), encoder_ptr.get_ptr());
465 }
466 })?;
467
468 slot.replace(encoder);
469
470 Ok(())
471 }
472
473 fn create_and_set_audio_encoder(
475 &self,
476 info: AudioEncoderInfo,
477 mixer_idx: usize,
478 ) -> Result<Arc<ObsAudioEncoder>, ObsError> {
479 validate_audio_mixer(mixer_idx)?;
480 if self.is_active()? {
481 return Err(ObsError::OutputAlreadyActive);
482 }
483 ensure_output_supports_audio_encoder(self, self.native_output_flags()?)?;
484
485 let audio_enc = ObsAudioEncoder::new_from_info(info, mixer_idx, self.runtime().clone())?;
486 self.set_audio_encoder(audio_enc.clone(), mixer_idx)?;
487 Ok(audio_enc)
488 }
489
490 fn set_audio_encoder(
494 &self,
495 encoder: Arc<ObsAudioEncoder>,
496 mixer_idx: usize,
497 ) -> Result<(), ObsError> {
498 let _configuration = self
499 .configuration_lock()
500 .lock()
501 .map_err(|e| ObsError::LockError(e.to_string()))?;
502 validate_audio_mixer(mixer_idx)?;
503 if self.is_active()? {
504 return Err(ObsError::OutputAlreadyActive);
505 }
506 ensure_output_supports_audio_encoder(self, self.native_output_flags()?)?;
507
508 self.runtime().ensure_same_runtime(encoder.runtime())?;
509
510 let mut slots = self
511 .audio_encoder_slots()
512 .write()
513 .map_err(|e| ObsError::LockError(e.to_string()))?;
514 let encoder_ptr = encoder.__native_handle();
515 let output_ptr = self.__native_handle();
516 let runtime = self.runtime().clone();
517 run_with_obs!(runtime, (output_ptr, encoder_ptr), move || {
518 unsafe {
519 libobs::obs_output_set_audio_encoder(
521 output_ptr.get_ptr(),
522 encoder_ptr.get_ptr(),
523 mixer_idx,
524 );
525 }
526 })?;
527
528 slots.insert(mixer_idx, encoder);
529
530 Ok(())
531 }
532
533 fn start(&self) -> Result<(), ObsError> {
536 let _configuration = self
537 .configuration_lock()
538 .lock()
539 .map_err(|e| ObsError::LockError(e.to_string()))?;
540 if self.is_active()? {
541 return Err(ObsError::OutputAlreadyActive);
542 }
543
544 let vid_encoder_ptr = self
545 .video_encoder_slot()
546 .read()
547 .map_err(|e| ObsError::LockError(e.to_string()))?
548 .as_ref()
549 .map(|enc| enc.__native_handle());
550
551 let audio_encoder_pointers = self
552 .audio_encoder_slots()
553 .read()
554 .map_err(|e| ObsError::LockError(e.to_string()))?
555 .values()
556 .map(|enc| enc.__native_handle())
557 .collect::<Vec<_>>();
558
559 let output_ptr = self.__native_handle();
560 let runtime = self.runtime().clone();
561 let res = run_with_obs!(
562 runtime,
563 (output_ptr, vid_encoder_ptr, audio_encoder_pointers),
564 move || {
565 if let Some(vid_encoder_ptr) = vid_encoder_ptr {
566 unsafe {
567 libobs::obs_encoder_set_video(
569 vid_encoder_ptr.get_ptr(),
570 libobs::obs_get_video(),
571 );
572 }
573 }
574 for audio_encoder_ptr in audio_encoder_pointers {
575 unsafe {
576 libobs::obs_encoder_set_audio(
578 audio_encoder_ptr.get_ptr(),
579 libobs::obs_get_audio(),
580 );
581 }
582 }
583
584 unsafe {
585 libobs::obs_output_start(output_ptr.get_ptr())
587 }
588 }
589 )?;
590
591 if res {
592 return Ok(());
593 }
594
595 let runtime = self.runtime().clone();
596 let err = run_with_obs!(runtime, (output_ptr), move || {
597 let err = unsafe {
598 libobs::obs_output_get_last_error(output_ptr.get_ptr())
600 };
601
602 if err.is_null() {
603 return "Unknown error".to_string();
604 }
605
606 let err = unsafe { CStr::from_ptr(err) };
607
608 let err = err.to_string_lossy().to_string();
609 err
610 })?;
611
612 Err(ObsError::OutputStartFailure(Some(err)))
613 }
614
615 fn set_paused(&self, should_pause: bool) -> Result<(), ObsError> {
616 if self.runtime().is_actor_thread() {
617 return Err(ObsError::RuntimeReentrantBlocking);
618 }
619 if !self.is_active()? {
620 return Err(ObsError::OutputPauseFailure(Some(
621 "Output is not active.".to_string(),
622 )));
623 }
624
625 let output_ptr = self.__native_handle();
626 let runtime = self.runtime().clone();
627
628 let rx = if should_pause {
629 self.signals().on_pause()?
630 } else {
631 self.signals().on_unpause()?
632 };
633
634 let res = run_with_obs!(runtime, (output_ptr), move || {
635 unsafe {
636 libobs::obs_output_pause(output_ptr.get_ptr(), should_pause)
638 }
639 })?;
640
641 if res {
642 rx.blocking_recv().map_err(|_| ObsError::NoSenderError)?;
643
644 Ok(())
645 } else {
646 let runtime = self.runtime().clone();
647 let err = run_with_obs!(runtime, (output_ptr), move || {
648 let err = unsafe {
649 libobs::obs_output_get_last_error(output_ptr.get_ptr())
651 };
652
653 if err.is_null() {
654 return None;
655 }
656
657 let err = unsafe { CStr::from_ptr(err) };
658 let err = err.to_string_lossy().to_string();
659
660 Some(err)
661 })?;
662
663 Err(ObsError::OutputPauseFailure(err))
664 }
665 }
666
667 fn pause(&self) -> Result<(), ObsError> {
669 self.set_paused(true)
670 }
671
672 fn unpause(&self) -> Result<(), ObsError> {
673 self.set_paused(false)
674 }
675
676 fn stop(&self) -> Result<(), ObsError> {
678 if self.runtime().is_actor_thread() {
679 return Err(ObsError::RuntimeReentrantBlocking);
680 }
681 let _configuration = self
682 .configuration_lock()
683 .lock()
684 .map_err(|e| ObsError::LockError(e.to_string()))?;
685 let output_ptr = self.__native_handle();
686 let runtime = self.runtime().clone();
687 let output_active = run_with_obs!(runtime, (output_ptr), move || {
688 unsafe {
689 libobs::obs_output_active(output_ptr.get_ptr())
691 }
692 })?;
693
694 if !output_active {
695 return Err(ObsError::OutputStopFailure(Some(
696 "Output is not active.".to_string(),
697 )));
698 }
699
700 let rx = self.signals().on_stop()?;
701 let rx_deactivate = self.signals().on_deactivate()?;
702
703 let runtime = self.runtime().clone();
704 run_with_obs!(runtime, (output_ptr), move || {
705 unsafe {
706 libobs::obs_output_stop(output_ptr.get_ptr())
708 }
709 })?;
710
711 let signal = rx.blocking_recv().map_err(|_| ObsError::NoSenderError)?;
712
713 log::trace!("Received stop signal: {:?}", signal);
714 if signal != ObsOutputStopSignal::Success {
715 return Err(ObsError::OutputStopFailure(Some(signal.to_string())));
716 }
717
718 rx_deactivate
719 .blocking_recv()
720 .map_err(|_| ObsError::NoSenderError)?;
721
722 Ok(())
723 }
724
725 fn is_active(&self) -> Result<bool, ObsError> {
727 let output_ptr = self.__native_handle();
728 let runtime = self.runtime().clone();
729 let output_active = run_with_obs!(runtime, (output_ptr), move || {
730 unsafe {
731 libobs::obs_output_active(output_ptr.get_ptr())
733 }
734 })?;
735
736 Ok(output_active)
737 }
738}
739
740fn output_supports_video_encoder(flags: u32) -> bool {
741 flags & libobs::OBS_OUTPUT_ENCODED != 0 && flags & libobs::OBS_OUTPUT_VIDEO != 0
742}
743
744fn output_supports_audio_encoder(flags: u32) -> bool {
745 flags & libobs::OBS_OUTPUT_ENCODED != 0 && flags & libobs::OBS_OUTPUT_AUDIO != 0
746}
747
748fn output_supports_service(flags: u32) -> bool {
749 flags & libobs::OBS_OUTPUT_SERVICE != 0
750}
751
752fn unsupported_component<T: ObsOutputTrait + ?Sized>(output: &T, component: &str) -> ObsError {
753 ObsError::OutputPipelineUnexpectedComponent {
754 output_id: output.id().to_string(),
755 component: component.to_owned(),
756 }
757}
758
759fn ensure_output_supports_video_encoder<T: ObsOutputTrait + ?Sized>(
760 output: &T,
761 flags: u32,
762) -> Result<(), ObsError> {
763 output_supports_video_encoder(flags)
764 .then_some(())
765 .ok_or_else(|| unsupported_component(output, "a video encoder"))
766}
767
768fn ensure_output_supports_audio_encoder<T: ObsOutputTrait + ?Sized>(
769 output: &T,
770 flags: u32,
771) -> Result<(), ObsError> {
772 output_supports_audio_encoder(flags)
773 .then_some(())
774 .ok_or_else(|| unsupported_component(output, "audio encoders"))
775}
776
777fn ensure_output_supports_service<T: ObsOutputTrait + ?Sized>(
778 output: &T,
779 flags: u32,
780) -> Result<(), ObsError> {
781 output_supports_service(flags)
782 .then_some(())
783 .ok_or_else(|| unsupported_component(output, "a service"))
784}
785
786fn validate_audio_mixer(mixer_idx: usize) -> Result<(), ObsError> {
787 if mixer_idx >= libobs::MAX_AUDIO_MIXES as usize {
788 return Err(ObsError::AudioMixerIndexOutOfBounds {
789 index: mixer_idx,
790 max: (libobs::MAX_AUDIO_MIXES - 1) as usize,
791 });
792 }
793 Ok(())
794}
795
796#[cfg(test)]
797mod tests {
798 use super::validate_audio_mixer;
799
800 #[test]
801 fn audio_mixer_indices_are_bounded_before_ffi() {
802 assert!(validate_audio_mixer(0).is_ok());
803 assert!(validate_audio_mixer((libobs::MAX_AUDIO_MIXES - 1) as usize).is_ok());
804 assert!(validate_audio_mixer(libobs::MAX_AUDIO_MIXES as usize).is_err());
805 }
806}